HR project¶
Table of Contents
- 1 Описание проекта
- 2 Задача 1: Предсказание уровня удовлетворённости сотрудника
- 3 Задача 2: предсказание увольнения сотрудника из компании
- 3.1 Загрузка данных
- 3.2 Предобработка данных
- 3.3 Исследовательский анализ данных
- 3.3.1 Объединение тестовой выборки для статистического анализа
- 3.3.2 Анализ количественных и качественных признаков
- 3.3.3 Корреляционный анализ
- 3.3.4 Корреляционный анализ
- 3.3.5 Портрет уходящего работника
- 3.3.6 Визуализация и сравнение распределения признака job_satisfaction_rate для ушедших и оставшихся сотрудников
- 3.4 Добавление нового входного признака
- 3.5 Подготовка данных
- 3.6 Обучение модели
- 3.7 Оформление выводов
- 4 Общий вывод:
Описание проекта¶
HR-аналитики компании «Работа с заботой» помогают бизнесу оптимизировать управление персоналом: бизнес предоставляет данные, а аналитики предлагают, как избежать финансовых потерь и оттока сотрудников. В этом HR-аналитикам пригодится машинное обучение, с помощью которого получится быстрее и точнее отвечать на вопросы бизнеса.
Компания предоставила данные с характеристиками сотрудников компании. Среди них — уровень удовлетворённости сотрудника работой в компании. Эту информацию получили из форм обратной связи: сотрудники заполняют тест-опросник, и по его результатам рассчитывается доля их удовлетворённости от 0 до 1, где 0 — совершенно неудовлетворён, 1 — полностью удовлетворён.
Собирать данные такими опросниками не так легко: компания большая, и всех сотрудников надо сначала оповестить об опросе, а затем проследить, что все его прошли.
У нас будет несколько задач:
Первая задача — построить модель, которая сможет предсказать уровень удовлетворённости сотрудника на основе данных заказчика.
Почему бизнесу это важно: удовлетворённость работой напрямую влияет на отток сотрудников. А предсказание оттока — одна из важнейших задач HR-аналитиков. Внезапные увольнения несут в себе риски для компании, особенно если уходит важный сотрудник.
Вторая задача — построить модель, которая сможет на основе данных заказчика предсказать то, что сотрудник уволится из компании.
Задача 1: Предсказание уровня удовлетворённости сотрудника¶
Описание данных¶
Для этой задачи заказчик предоставил данные с признаками:
id— уникальный идентификатор сотрудникаdept— отдел, в котором работает сотрудникlevel— уровень занимаемой должностиworkload— уровень загруженности сотрудникаemployment_years— длительность работы в компании (в годах)last_year_promo— показывает, было ли повышение за последний годlast_year_violations— показывает, нарушал ли сотрудник трудовой договор за последний годsupervisor_evaluation— оценка качества работы сотрудника, которую дал руководительsalary— ежемесячная зарплата сотрудникаjob_satisfaction_rate— уровень удовлетворённости сотрудника работой в компании, целевой признак
Имортирование необходимых для анализа библиотек
%%capture
!pip install -U scikit-learn
!pip -q install phik
!pip -q install shap
!pip -q install pandas
import os
import pandas as pd
import matplotlib.pyplot as plt
import math
import numpy as np
import re
import seaborn as sns
import shap
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)
from scipy.stats import shapiro
from phik import phik_matrix
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import (
StandardScaler, OneHotEncoder,
MinMaxScaler, OrdinalEncoder, LabelEncoder
)
from sklearn.impute import SimpleImputer
from sklearn.model_selection import GridSearchCV
from sklearn.dummy import DummyRegressor, DummyClassifier
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.tree import DecisionTreeRegressor, DecisionTreeClassifier
from sklearn.svm import SVR, SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import roc_auc_score, make_scorer
# Настройки вывода графиков
plt.rcParams["axes.titlesize"] = 16 # Размер шрифта
plt.rcParams["axes.titleweight"] = "bold" # Толщина шрифта
# Константы
RANDOM_STATE = 42
try:
TERM_SIZE = os.get_terminal_size()
except OSError:
TERM_SIZE = os.terminal_size((80, 24)) # Размер по умолчанию
Загрузка данных¶
try:
train_job_satisfaction_rate = pd.read_csv("C:\\Data-science\\ds_csv\\train_job_satisfaction_rate.csv")
test_features = pd.read_csv("C:\\Data-science\\ds_csv\\test_features.csv")
test_target_job_satisfaction_rate = pd.read_csv("C:\\Data-science\\ds_csv\\test_target_job_satisfaction_rate.csv")
except:
try:
train_job_satisfaction_rate = pd.read_csv('/datasets/train_job_satisfaction_rate.csv')
test_features = pd.read_csv('/datasets/test_features.csv')
test_target_job_satisfaction_rate = pd.read_csv('/datasets/test_target_job_satisfaction_rate.csv')
except:
raise FileNotFoundError
Изучение загруженных датасетов¶
# Создаем словарь, чтобы перебрать все импортируемые датафреймы
dataframes = {
"train_job_satisfaction_rate": train_job_satisfaction_rate,
"test_features": test_features,
"test_target_job_satisfaction_rate": test_target_job_satisfaction_rate
}
# Цикл по каждому DataFrame с выводом имени, информации и первых строк
for name, data in dataframes.items():
print(f'\033[1mНаименование анализируемого датафрейма:\033[0m {name}')
print()
data.info() # Выводим информацию о DataFrame
display(data.head(5)) # Отображаем первые 5 строк
print('=' * TERM_SIZE.columns) # Линия-разделитель по ширине терминала
Наименование анализируемого датафрейма: train_job_satisfaction_rate
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4000 entries, 0 to 3999
Data columns (total 10 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 4000 non-null int64
1 dept 3994 non-null object
2 level 3996 non-null object
3 workload 4000 non-null object
4 employment_years 4000 non-null int64
5 last_year_promo 4000 non-null object
6 last_year_violations 4000 non-null object
7 supervisor_evaluation 4000 non-null int64
8 salary 4000 non-null int64
9 job_satisfaction_rate 4000 non-null float64
dtypes: float64(1), int64(4), object(5)
memory usage: 312.6+ KB
| id | dept | level | workload | employment_years | last_year_promo | last_year_violations | supervisor_evaluation | salary | job_satisfaction_rate | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 155278 | sales | junior | medium | 2 | no | no | 1 | 24000 | 0.58 |
| 1 | 653870 | hr | junior | high | 2 | no | no | 5 | 38400 | 0.76 |
| 2 | 184592 | sales | junior | low | 1 | no | no | 2 | 12000 | 0.11 |
| 3 | 171431 | technology | junior | low | 4 | no | no | 2 | 18000 | 0.37 |
| 4 | 693419 | hr | junior | medium | 1 | no | no | 3 | 22800 | 0.20 |
===============================================================================================================
Наименование анализируемого датафрейма: test_features
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2000 entries, 0 to 1999
Data columns (total 9 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 2000 non-null int64
1 dept 1998 non-null object
2 level 1999 non-null object
3 workload 2000 non-null object
4 employment_years 2000 non-null int64
5 last_year_promo 2000 non-null object
6 last_year_violations 2000 non-null object
7 supervisor_evaluation 2000 non-null int64
8 salary 2000 non-null int64
dtypes: int64(4), object(5)
memory usage: 140.8+ KB
| id | dept | level | workload | employment_years | last_year_promo | last_year_violations | supervisor_evaluation | salary | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 485046 | marketing | junior | medium | 2 | no | no | 5 | 28800 |
| 1 | 686555 | hr | junior | medium | 1 | no | no | 4 | 30000 |
| 2 | 467458 | sales | middle | low | 5 | no | no | 4 | 19200 |
| 3 | 418655 | sales | middle | low | 6 | no | no | 4 | 19200 |
| 4 | 789145 | hr | middle | medium | 5 | no | no | 5 | 40800 |
===============================================================================================================
Наименование анализируемого датафрейма: test_target_job_satisfaction_rate
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2000 entries, 0 to 1999
Data columns (total 2 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 2000 non-null int64
1 job_satisfaction_rate 2000 non-null float64
dtypes: float64(1), int64(1)
memory usage: 31.4 KB
| id | job_satisfaction_rate | |
|---|---|---|
| 0 | 130604 | 0.74 |
| 1 | 825977 | 0.75 |
| 2 | 418490 | 0.60 |
| 3 | 555320 | 0.72 |
| 4 | 826430 | 0.08 |
===============================================================================================================
Определение наличия и количества пропусков в датасетах¶
# Цикл по каждому DataFrame с выводом имени, информации и первых строк
for name, data in dataframes.items():
print(f'\033[1mНаименование анализируемого датафрейма:\033[0m {name}')
display(data.isna().sum()) # Отображаем первые 5 строк
print('=' * TERM_SIZE.columns) # Линия-разделитель по ширине терминала
Наименование анализируемого датафрейма: train_job_satisfaction_rate
id 0 dept 6 level 4 workload 0 employment_years 0 last_year_promo 0 last_year_violations 0 supervisor_evaluation 0 salary 0 job_satisfaction_rate 0 dtype: int64
===============================================================================================================
Наименование анализируемого датафрейма: test_features
id 0 dept 2 level 1 workload 0 employment_years 0 last_year_promo 0 last_year_violations 0 supervisor_evaluation 0 salary 0 dtype: int64
===============================================================================================================
Наименование анализируемого датафрейма: test_target_job_satisfaction_rate
id 0 job_satisfaction_rate 0 dtype: int64
===============================================================================================================
Вывод по предварительному анализу:
В ходе предварительного анализа данных было выявлено:
- В датафреймах присутствуют пропуски;
- Название столбцов имеют форму snake_case.
Предобработка данных¶
Проверка датафреймов на явные дубликаты¶
for name, data in dataframes.items():
print(f"Явных дубликатов в датасете {name} - {data.duplicated().sum()}: "
if data.duplicated().sum() > 0 else f"Явных дубликатов в датасете {name} - НЕТ")
Явных дубликатов в датасете train_job_satisfaction_rate - НЕТ Явных дубликатов в датасете test_features - НЕТ Явных дубликатов в датасете test_target_job_satisfaction_rate - НЕТ
Проверка датафреймов на неявные дубликаты¶
for name, data in dataframes.items():
col_cat = data.select_dtypes(include=['object']).columns.to_list()
print(f'\033[1mНазвание датафрейма {name}\033[0m')
for col in col_cat:
print(f"Уникальные значения в столбце '{col}': {data[col].unique()}")
print('=' * TERM_SIZE.columns)
Название датафрейма train_job_satisfaction_rate Уникальные значения в столбце 'dept': ['sales' 'hr' 'technology' 'purchasing' 'marketing' nan] Уникальные значения в столбце 'level': ['junior' 'middle' 'sinior' nan] Уникальные значения в столбце 'workload': ['medium' 'high' 'low'] Уникальные значения в столбце 'last_year_promo': ['no' 'yes'] Уникальные значения в столбце 'last_year_violations': ['no' 'yes'] =============================================================================================================== Название датафрейма test_features Уникальные значения в столбце 'dept': ['marketing' 'hr' 'sales' 'purchasing' 'technology' nan ' '] Уникальные значения в столбце 'level': ['junior' 'middle' 'sinior' nan] Уникальные значения в столбце 'workload': ['medium' 'low' 'high' ' '] Уникальные значения в столбце 'last_year_promo': ['no' 'yes'] Уникальные значения в столбце 'last_year_violations': ['no' 'yes'] =============================================================================================================== Название датафрейма test_target_job_satisfaction_rate ===============================================================================================================
Заменим пропуски датафрейма test_features на значение Nan
test_features['dept'] = test_features['dept'].replace(' ', np.nan)
test_features['workload'] = test_features['workload'].replace(' ', np.nan)
test_features['level'] = test_features['level'].replace('sinior', 'senior')
Исправление опечатки в наименовании должности senior
train_job_satisfaction_rate['level'] = train_job_satisfaction_rate['level'].replace('sinior', 'senior')
test_features['level'] = test_features['level'].replace('sinior', 'senior')
col_cat = test_features.select_dtypes(include=['object']).columns.to_list()
for col in col_cat:
print(f"Уникальные значения в столбце '{col}': {test_features[col].unique()}")
Уникальные значения в столбце 'dept': ['marketing' 'hr' 'sales' 'purchasing' 'technology' nan] Уникальные значения в столбце 'level': ['junior' 'middle' 'senior' nan] Уникальные значения в столбце 'workload': ['medium' 'low' 'high' nan] Уникальные значения в столбце 'last_year_promo': ['no' 'yes'] Уникальные значения в столбце 'last_year_violations': ['no' 'yes']
# Обновление словаря с данными
dataframes = {
"train_job_satisfaction_rate": train_job_satisfaction_rate,
"test_features": test_features,
"test_target_job_satisfaction_rate": test_target_job_satisfaction_rate
}
for name, data in dataframes.items():
print(f"Явных дубликатов в датасете {name} - {data.duplicated().sum()}: "
if data.duplicated().sum() > 0 else f"Явных дубликатов в датасете {name} - НЕТ")
Явных дубликатов в датасете train_job_satisfaction_rate - НЕТ Явных дубликатов в датасете test_features - НЕТ Явных дубликатов в датасете test_target_job_satisfaction_rate - НЕТ
Вывод по предобработке данных:
В ходе предобработке данных была выполненна проверка датафреймов на дубликаты и пропуски, в результате которой все пропуски были заменены на значение Nan, опечатка в слове senior исправлена
Исследовательский анализ данных¶
Функции для исследовательского анализа данных¶
def show_num_variable(df, column, target=None, suptitle=None):
'''
Функция отображения гистограммы распределения
и диаграммы размаха для определенного столбца датафрейма
с учетом принадлежности данного столбца к разным значениям
переменной target.
Параметры:
- df: pandas.DataFrame, входной датафрейм
- column: str, столбец для анализа
- target: str или None, столбец для группировки (по умолчанию None)
'''
sns.set()
f, axes = plt.subplots(1, 2, figsize=(16, 6))
# Гистограмма
axes[0].set_title(f'Гистограмма для {column}', fontsize=16)
if target:
axes[0].set_ylabel('Плотность', fontsize=14)
sns.histplot(data=df, bins=20, kde=True, ax=axes[0], hue=target, x=column, stat='density', common_norm=False)
else:
axes[0].set_ylabel('Количество', fontsize=14)
sns.histplot(data=df, bins=20, kde=True, ax=axes[0], x=column)
# Диаграмма размаха
axes[1].set_title(f'Диаграмма размаха для {column}', fontsize=16)
if target:
sns.boxplot(data=df, ax=axes[1], x=target, y=column)
else:
sns.boxplot(data=df, ax=axes[1], y=column, orient='v')
axes[1].set_ylabel(column, fontsize=14)
# Добавляем главный заголовок
if suptitle:
plt.suptitle(f'{suptitle}', fontsize=18, fontweight='bold')
plt.tight_layout()
plt.show()
def show_discrete_variable(df, column, target=None):
'''
Функция отображения гистограммы распределения для дискретного столбца датафрейма.
Если задан target, строит отдельные графики для каждой группы.
Параметры:
- df: pandas.DataFrame, входной датафрейм
- column: str, столбец для анализа (категориальный)
- target: str или None, целевая переменная для разбиения (по умолчанию None)
'''
# Проверяем наличие столбцов в датафрейме
if column not in df.columns:
raise ValueError(f"Столбец '{column}' отсутствует в датафрейме.")
if target and target not in df.columns:
raise ValueError(f"Столбец '{target}' отсутствует в датафрейме.")
# Если целевой переменной нет, строим обычный countplot
if target is None:
plt.figure(figsize=(10, 6))
plt.title(f'Распределение значений {column}', fontsize=16)
ax = sns.countplot(data=df, x=column)
add_percentages(ax, df, column) # Добавляем проценты
ax.set_ylabel("Частота (%)", fontsize=14)
plt.show()
return
# Получаем уникальные значения целевой переменной
unique_values = df[target].unique()
# Создаём два графика рядом
fig, axes = plt.subplots(1, len(unique_values), figsize=(12, 6), sharey=True)
for ax, value in zip(axes, unique_values):
subset = df[df[target] == value] # Выборка по значению target
ax.set_title(f"{target} = {value}", fontsize=14)
sns.countplot(data=subset, x=column, ax=ax)
add_percentages(ax, subset, column) # Добавляем проценты
ax.set_xlabel(column, fontsize=12)
axes[0].set_ylabel("Частота (%)", fontsize=14) # ось Y
axes[1].set_ylabel("Частота (%)", fontsize=14) # ось Y
plt.tight_layout()
plt.show()
def add_percentages(ax, df, column):
"""Функция для добавления процентов над столбцами"""
total = len(df)
for p in ax.patches:
count = p.get_height()
if count > 0:
percentage = f'{100 * count / total:.1f}%'
ax.annotate(percentage,
(p.get_x() + p.get_width() / 2, p.get_height()),
ha='center', va='bottom', fontsize=12, color='black')
def show_cat_variable_by_target(df, column, title, target=None, rot=60):
'''
Функция отображения соотношения категориальных признаков
в столбце датафрейма, разделенных по значениям целевого признака.
Если target не передан, отображается только countplot для column.
Добавляет проценты на график.
:param df: DataFrame, датафрейм с данными
:param column: str, название столбца, по которому строится график
:param title: str, заголовок графика
:param target: str, название столбца с целевым признаком (по умолчанию None)
:param rot: int, угол поворота меток на оси X
'''
# Если target не указан, рисуем только countplot для одного признака
if target is None:
plt.figure(figsize=(12, 6))
ax = sns.countplot(data=df, x=column)
ax.set_title(f"{title}", fontsize=14)
ax.set_xlabel(column, fontsize=12)
ax.set_ylabel('Количество', fontsize=12)
ax.tick_params(axis='x', rotation=rot)
# Добавляем проценты на график
total_count = len(df)
for p in ax.patches:
count = p.get_height()
if count > 0: # Добавляем проценты только если высота столбца > 0
percentage = f'{100 * count / total_count:.1f}%'
ax.annotate(percentage,
(p.get_x() + p.get_width() / 2., count),
ha='center', va='center',
xytext=(0, 10),
textcoords='offset points',
fontsize=12, color='black')
plt.tight_layout()
plt.show()
return
# Проверяем, что столбец target существует в DataFrame
if target not in df.columns:
raise ValueError(f"Столбец '{target}' не найден в DataFrame.")
# Если target указан, строим графики для каждого уникального значения target
unique_targets = df[target].unique()
num_targets = len(unique_targets)
# Определяем размер холста в зависимости от количества таргетов
fig, axes = plt.subplots(nrows=num_targets, ncols=1, figsize=(12, 5 * num_targets), sharex=True)
# Если уникальных значений больше одного, axes будет массивом
# Если только одно значение, делаем axes списком для унификации
if num_targets == 1:
axes = [axes]
# Создаем график для каждого уникального значения целевого признака
for i, target_value in enumerate(unique_targets):
# Создаем подмножество данных для текущего значения целевого признака
subset = df[df[target] == target_value]
# Получаем уникальные значения категориального признака для сортировки
categories = subset[column].value_counts().index.tolist()
# Создаем countplot для текущего значения целевого признака
ax = sns.countplot(data=subset, x=column, ax=axes[i], order=categories)
# Заголовок и настройка осей
axes[i].set_title(f"{title}: {target} - {target_value}", fontsize=14)
axes[i].set_xlabel(column if i == num_targets - 1 else "", fontsize=12) # Подпись только для нижнего графика
axes[i].set_ylabel('Доля сотрудников', fontsize=12)
axes[i].tick_params(axis='x', rotation=rot)
# Вычисляем общее количество для текущей группы
total_count = len(subset)
# Добавляем проценты на столбцы
for p in ax.patches:
count = p.get_height()
if count > 0: # Добавляем проценты только если высота столбца > 0
percentage = f'{100 * count / total_count:.1f}%'
# Вычисляем координаты для аннотации (верхняя часть столбца)
ax.annotate(percentage,
(p.get_x() + p.get_width() / 2., count),
ha='center', va='center',
xytext=(0, 10),
textcoords='offset points',
fontsize=12, color='black')
# Улучшаем отображение
plt.tight_layout(h_pad=2.0) # Добавляем вертикальный отступ между графиками
plt.show()
def normal_check(data, column, alpha=0.05):
'''
Функция проверки нормальности распределения
по тесту Шапиро — Уилка
с обработкой больших наборов данных.
'''
print(f"""
Выдвенем гипотезы:
- Н0: Распределение параметра {column} является нормальным.
- Н1: Распределение параметра {column} не является нормальным.
""")
sample = data[column]
# Тест Шапиро-Уилка
stat, p = shapiro(sample)
print(f"Тест Шапиро — Уилка: Stat={stat:.3f}, p={p:.3g}")
# Результат
if p > alpha:
return print(f"Распределение данных нормальное с вероятностью более {1 - alpha:.2f}. Не получилось отвергнуть нулевую гипотезу")
else:
return print(f"Распределение данных не нормальное с вероятностью более {1 - alpha:.2f}. Отвергаем нулевую гипотезу")
Объединение тестовой выборки для статистического анализа¶
test_full = test_features.merge(test_target_job_satisfaction_rate, on='id', how='inner')
display(test_full.shape)
test_full.sample(5)
(2000, 10)
| id | dept | level | workload | employment_years | last_year_promo | last_year_violations | supervisor_evaluation | salary | job_satisfaction_rate | |
|---|---|---|---|---|---|---|---|---|---|---|
| 695 | 116186 | technology | junior | medium | 1 | no | no | 4 | 26400 | 0.55 |
| 1203 | 393065 | purchasing | junior | high | 1 | no | no | 2 | 37200 | 0.27 |
| 1953 | 196670 | sales | middle | high | 7 | no | no | 2 | 45600 | 0.29 |
| 530 | 612023 | purchasing | junior | medium | 1 | no | no | 3 | 25200 | 0.31 |
| 992 | 308908 | technology | middle | low | 7 | no | no | 3 | 21600 | 0.32 |
Анализ количественных и качественных признаков¶
Для тренировочных данных train_job_satisfaction_rate¶
# Формирование списка столбцов с количественными признаками
num_variables_col = train_job_satisfaction_rate.select_dtypes(include=['number']).columns.to_list()
num_variables_col = [col for col in num_variables_col if col not in ['id', 'supervisor_evaluation', 'employment_years']]
num_variables_col
['salary', 'job_satisfaction_rate']
# Формирование списка столбцов с количественными дискретными признаками
discrete_variables_col = ['supervisor_evaluation', 'employment_years']
discrete_variables_col
['supervisor_evaluation', 'employment_years']
# Вывод графиков для датафрейма train_job_satisfaction_rate
for col in num_variables_col:
show_num_variable(train_job_satisfaction_rate, col)
normal_check(train_job_satisfaction_rate, col)
print('=' * TERM_SIZE.columns)
Выдвенем гипотезы:
- Н0: Распределение параметра salary является нормальным.
- Н1: Распределение параметра salary не является нормальным.
Тест Шапиро — Уилка: Stat=0.939, p=8.39e-38
Распределение данных не нормальное с вероятностью более 0.95. Отвергаем нулевую гипотезу
===============================================================================================================
Выдвенем гипотезы:
- Н0: Распределение параметра job_satisfaction_rate является нормальным.
- Н1: Распределение параметра job_satisfaction_rate не является нормальным.
Тест Шапиро — Уилка: Stat=0.971, p=8.95e-28
Распределение данных не нормальное с вероятностью более 0.95. Отвергаем нулевую гипотезу
===============================================================================================================
train_job_satisfaction_rate[num_variables_col].describe().round(3).T
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| salary | 4000.0 | 33926.700 | 14900.704 | 12000.00 | 22800.00 | 30000.00 | 43200.00 | 98400.0 |
| job_satisfaction_rate | 4000.0 | 0.534 | 0.225 | 0.03 | 0.36 | 0.56 | 0.71 | 1.0 |
Вывод для количесвенных значений датафрейма train_job_satisfaction_rate:
- Распределения количественных значений датафрейма train_job_satisfaction_rate отличаются от Гауссовсского;
- Для
salary(ежемесячная заработная плата) наиболее популярны значения от 20000 до 30000. Так же были замечены выбросы с зарплатами более 73800 для отдельных сеньеров. Среднее значениеsalary-mean = 33927, медианное значениеsalary-median = 30000; - Для
job_satisfaction_rate(уровень удовлетворённости сотрудника работой в компании) наиболее популярны значения от 0.6 до 0.8. Среднее значениеjob_satisfaction_rate-mean = 0.534, медианное значениеjob_satisfaction_rate-median = 0.56.
# Вывод графиков для дискретных значений датафрейма train_job_satisfaction_rate
for col in discrete_variables_col:
show_discrete_variable(train_job_satisfaction_rate, col)
print('=' * TERM_SIZE.columns)
===============================================================================================================
===============================================================================================================
train_job_satisfaction_rate[discrete_variables_col].describe().round(3).T
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| supervisor_evaluation | 4000.0 | 3.476 | 1.009 | 1.0 | 3.0 | 4.0 | 4.0 | 5.0 |
| employment_years | 4000.0 | 3.718 | 2.543 | 1.0 | 2.0 | 3.0 | 6.0 | 10.0 |
Вывод для дискретных признаков в датафрейма train_job_satisfaction_rate:
- Для
supervisor_evaluation(оценка качества работы сотрудника) наиболее популярные оценки 3 и 4. Среднее значениеsupervisor_evaluation-mean = 3.476, медианное значениеsupervisor_evaluation-median = 4. Так же в ходе анализа было найдено отсутствие зависимости качество работы сотрудника от занимаемой им должности; - Для
employment_years(длительности работы в компании) наблюдается, что наибольшее количество работников проработали в компании 1 год, в ней также отсутствуют работники без опыта, в целом заметен ниспадающий тренд чем больше стаж тем меньше работников данного стажа в компании. Среднее значениеemployment_years-mean = 3.718, медианное значениеemployment_years-median = 3.
Выведем строки выбросов по salary
# Вычисляем квартили и интерквартильный размах (IQR)
Q1 = train_job_satisfaction_rate['salary'].quantile(0.25) # Первый квартиль
Q3 = train_job_satisfaction_rate['salary'].quantile(0.75) # Третий квартиль
IQR = Q3 - Q1 # Интерквартильный размах
# Верхняя граница диаграмы размаха
upper_bound = Q3 + 1.5 * IQR
display(upper_bound)
top_salary = train_job_satisfaction_rate.query('salary > @upper_bound')
display(top_salary.shape[0])
top_salary.sample(10)
73800.0
60
| id | dept | level | workload | employment_years | last_year_promo | last_year_violations | supervisor_evaluation | salary | job_satisfaction_rate | |
|---|---|---|---|---|---|---|---|---|---|---|
| 2717 | 681745 | technology | senior | high | 5 | no | yes | 3 | 76800 | 0.03 |
| 3150 | 811299 | marketing | senior | high | 8 | no | no | 4 | 75600 | 0.64 |
| 3570 | 498123 | purchasing | senior | high | 8 | no | no | 4 | 79200 | 0.88 |
| 1982 | 978915 | technology | senior | high | 7 | no | no | 1 | 92400 | 0.45 |
| 3571 | 562085 | sales | senior | high | 1 | no | no | 3 | 75600 | 0.41 |
| 3016 | 147589 | technology | senior | high | 2 | no | no | 1 | 75600 | 0.16 |
| 3828 | 214696 | sales | senior | high | 5 | no | yes | 3 | 75600 | 0.13 |
| 2932 | 335600 | marketing | senior | high | 4 | no | no | 3 | 79200 | 0.31 |
| 1979 | 701051 | sales | senior | high | 2 | no | no | 3 | 78000 | 0.37 |
| 2146 | 229741 | sales | senior | high | 2 | no | no | 5 | 79200 | 0.66 |
top_salary['level'].unique()
array(['senior'], dtype=object)
Зависимость заработной платы от должности
show_num_variable(train_job_satisfaction_rate, 'salary', 'level')
Из гистограммы распределения плотности зарплат для разных должностей можно заметить связь значения зп от занимаемой должности.
Определим зависимость оценки качества работы сотрудника от его должности
train_job_satisfaction_rate.groupby('level')['supervisor_evaluation'].describe().round(3).T
| level | junior | middle | senior |
|---|---|---|---|
| count | 1894.00 | 1744.000 | 358.000 |
| mean | 3.48 | 3.481 | 3.430 |
| std | 1.01 | 1.006 | 1.015 |
| min | 1.00 | 1.000 | 1.000 |
| 25% | 3.00 | 3.000 | 3.000 |
| 50% | 4.00 | 4.000 | 4.000 |
| 75% | 4.00 | 4.000 | 4.000 |
| max | 5.00 | 5.000 | 5.000 |
# Лист с названиями столбцов категориальных признаков
dict_market_file_cat = train_job_satisfaction_rate.select_dtypes(include=['object']).columns.to_list()
for col in dict_market_file_cat:
show_cat_variable_by_target(train_job_satisfaction_rate, col, col)
print('=' * TERM_SIZE.columns)
===============================================================================================================
===============================================================================================================
===============================================================================================================
===============================================================================================================
===============================================================================================================
Вывод по категориальным данным датафрейма train_data_without_id:
- Больше всего сотрудников в компании работает в продажах (37.8 %), наименьшее число работает в hr;
- В компании малое количество высококвалифицированных сотрудников (senior = 8.9 %), сотрудников низкой и средней квалификации примерно равное количество (junior = 47.4 %, middle = 43.6 %);
- Большинство сотрудников в компании имеют среднюю загруженность (их 51.6 %), наименьшее количество высокозагруженных работников (их 18.4 %);
- В компании редко происходят повышения, за прошлый год повысили только 3 % сотрудников;
- Так же большинство сотрудников соблюдают правила установленные компанией в трудовом договоре (их 86 %).
Корреляционный анализ
Для корреляционного анализа и впоследствии модели машинного обучения, признак id не нужен, т.к. не несет никакой смысловой нагрузки, а так же он может повлиять на итоговую модель.
# Удаление столбца id
train_data_without_id = train_job_satisfaction_rate.drop('id', axis=1)
# Создание списка столбцов с непрерывно распределенными данными
col_names_corr = ['salary', 'job_satisfaction_rate']
big_data_corr = train_data_without_id.phik_matrix(interval_cols=col_names_corr)
plt.figure(figsize=(14, 10))
sns.heatmap(big_data_corr, annot=True, fmt=".2f", center=0)
plt.title('Матрица корреляций phik для датафрейма train_job_satisfaction_rate')
plt.show()
Вывод по корреляционному анализу для датафрейма train_job_satisfaction_rate
Мультиколлеанарности между признаками не замечено;
В ходе анализа Phik матрицы корреляций были определены силы связи целевого признака job_satisfaction_rate со входными, используя шкалу Чеддока:
- Высокая связь целевого признака прослеживается с входным признаком
supervisor_evaluation(0.76); - Средняя связь целевого признака прослеживается с входным признаком
last_year_violations(0.56): - Слабая связь целевого признака прослеживается с входным признаком
employment_years(0.33); - С остальными входными признаками связь очень слабая.
Для тестовых данных test_full¶
# Формирование списка столбцов с количественными признаками
num_variables_col = test_full.select_dtypes(include=['number']).columns.to_list()
num_variables_col = [col for col in num_variables_col if col not in ['id', 'supervisor_evaluation', 'employment_years']]
num_variables_col
['salary', 'job_satisfaction_rate']
# Формирование списка столбцов с количественными дискретными признаками
discrete_variables_col = ['supervisor_evaluation', 'employment_years']
discrete_variables_col
['supervisor_evaluation', 'employment_years']
# Вывод графиков для датафрейма test_full
for col in num_variables_col:
show_num_variable(test_full, col)
normal_check(test_full, col)
print('=' * TERM_SIZE.columns)
Выдвенем гипотезы:
- Н0: Распределение параметра salary является нормальным.
- Н1: Распределение параметра salary не является нормальным.
Тест Шапиро — Уилка: Stat=0.926, p=2.13e-30
Распределение данных не нормальное с вероятностью более 0.95. Отвергаем нулевую гипотезу
===============================================================================================================
Выдвенем гипотезы:
- Н0: Распределение параметра job_satisfaction_rate является нормальным.
- Н1: Распределение параметра job_satisfaction_rate не является нормальным.
Тест Шапиро — Уилка: Stat=0.970, p=5.12e-20
Распределение данных не нормальное с вероятностью более 0.95. Отвергаем нулевую гипотезу
===============================================================================================================
test_full[num_variables_col].describe().round(3).T
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| salary | 2000.0 | 34066.800 | 15398.437 | 12000.00 | 22800.00 | 30000.00 | 43200.00 | 96000.0 |
| job_satisfaction_rate | 2000.0 | 0.549 | 0.220 | 0.03 | 0.38 | 0.58 | 0.72 | 1.0 |
Вывод для количесвенных значений датафрейма test_full:
- Распределения количественных значений датафрейма test_full отличаются от Гауссовсского;
- Для
salary(ежемесячная заработная плата) наиболее популярны значения от 20000 до 30000. Так же были замечены выбросы с зарплатами более 73800 для отдельных сеньеров. Среднее значениеsalary-mean = 34067, медианное значениеsalary-median = 30000; - Для
job_satisfaction_rate(уровень удовлетворённости сотрудника работой в компании) наиболее популярны значения от 0.6 до 0.8. Среднее значениеjob_satisfaction_rate-mean = 0.549, медианное значениеjob_satisfaction_rate-median = 0.58.
# Вывод графиков для дискретных значений датафрейма test_full
for col in discrete_variables_col:
show_discrete_variable(test_full, col)
print('=' * TERM_SIZE.columns)
===============================================================================================================
===============================================================================================================
test_full[discrete_variables_col].describe().round(3).T
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| supervisor_evaluation | 2000.0 | 3.526 | 0.997 | 1.0 | 3.0 | 4.0 | 4.0 | 5.0 |
| employment_years | 2000.0 | 3.666 | 2.537 | 1.0 | 1.0 | 3.0 | 6.0 | 10.0 |
Вывод для дискретных признаков в датафрейма test_full:
- Для
supervisor_evaluation(оценка качества работы сотрудника) наиболее популярные оценки 3 и 4. Среднее значениеsupervisor_evaluation-mean = 3.526, медианное значениеsupervisor_evaluation-median = 4. Так же в ходе анализа было найдено отсутствие зависимости качество работы сотрудника от занимаемой им должности; - Для
employment_years(длительности работы в компании) наблюдается, что наибольшее количество работников проработали в компании 1 год, в ней также отсутствуют работники без опыта, в целом заметен ниспадающий тренд чем больше стаж тем меньше работников данного стажа в компании. Среднее значениеemployment_years-mean = 3.666, медианное значениеemployment_years-median = 3.
# Лист с названиями столбцов категориальных признаков
dict_market_file_cat = test_full.select_dtypes(include=['object']).columns.to_list()
for col in dict_market_file_cat:
show_cat_variable_by_target(test_full, col, col)
print('=' * TERM_SIZE.columns)
===============================================================================================================
===============================================================================================================
===============================================================================================================
===============================================================================================================
===============================================================================================================
Вывод для категориальных признаков в датафрейма test_full:
- Больше всего сотрудников в компании работает в продажах (38.1%), наименьшее число работает в hr;
- В компании малое количество высококвалифицированных сотрудников (senior = 8.8%), сотрудников низкой и средней квалификации примерно равное количество (junior = 48.7%, middle = 42.7%);
- Большинство сотрудников в компании имеют среднюю загруженность (их 52.1 %), наименьшее количество высокозагруженных работников (их 18.1%);
- В компании редко происходят повышения, за прошлый год повысили только 3.1 % сотрудников;
- Так же большинство сотрудников соблюдают правила установленные компанией в трудовом договоре (их 86.9%).
Корреляционный анализ¶
Для корреляционного анализа и впоследствии модели машинного обучения, признак id не нужен, т.к. не несет никакой смысловой нагрузки, а так же он может повлиять на итоговую модель.
# Удаление столбца id
test_data_without_id = test_full.drop('id', axis=1)
# Создание списка столбцов с непрерывно распределенными данными
col_names_corr = ['salary', 'job_satisfaction_rate']
big_data_corr = test_data_without_id.phik_matrix(interval_cols=col_names_corr)
plt.figure(figsize=(14, 10))
sns.heatmap(big_data_corr, annot=True, fmt=".2f", center=0)
plt.title('Матрица корреляций phik для датафрейма test_full')
plt.show()
Вывод по корреляционному анализу для датафрейма test_full
Мультиколлеанарности между признаками не замечено;
В ходе анализа Phik матрицы корреляций были определены силы связи целевого признака job_satisfaction_rate со входными, используя шкалу Чеддока:
- Высокая связь целевого признака прослеживается с входным признаком
supervisor_evaluation(0.77); - Средняя связь целевого признака прослеживается с входным признаком
last_year_violations(0.55): - Слабая связь целевого признака прослеживается с входным признаком
employment_years(0.31) иlast_year_promo; - С остальными входными признаками связь очень слабая.
Общий вывод по исследовательскому анализу
Для тренировочной выборки
Распределения количественных значений датафрейма train_job_satisfaction_rate отличаются от Гауссовсского;
Для
salary(ежемесячная заработная плата) наиболее популярны значения от 20000 до 30000. Так же были замечены выбросы с зарплатами более 73800 для отдельных сеньеров. Среднее значениеsalary-mean = 33927, медианное значениеsalary-median = 30000;Для
job_satisfaction_rate(уровень удовлетворённости сотрудника работой в компании) наиболее популярны значения от 0.6 до 0.8. Среднее значениеjob_satisfaction_rate-mean = 0.534, медианное значениеjob_satisfaction_rate-median = 0.56.Для
supervisor_evaluation(оценка качества работы сотрудника) наиболее популярные оценки 3 и 4. Среднее значениеsupervisor_evaluation-mean = 3.476, медианное значениеsupervisor_evaluation-median = 4. Так же в ходе анализа было найдено отсутствие зависимости качество работы сотрудника от занимаемой им должности;Для
employment_years(длительности работы в компании) наблюдается, что наибольшее количество работников проработали в компании 1 год, в ней также отсутствуют работники без опыта, в целом заметен ниспадающий тренд чем больше стаж тем меньше работников данного стажа в компании. Среднее значениеemployment_years-mean = 3.718, медианное значениеemployment_years-median = 3.Больше всего сотрудников в компании работает в продажах (37.8 %), наименьшее число работает в hr;
В компании малое количество высококвалифицированных сотрудников (senior = 8.9 %), сотрудников низкой и средней квалификации примерно равное количество (junior = 47.4 %, middle = 43.6 %);
Большинство сотрудников в компании имеют среднюю загруженность (их 51.6 %), наименьшее количество высокозагруженных работников (их 18.4 %);
В компании редко происходят повышения, за прошлый год повысили только 3 % сотрудников;
Так же большинство сотрудников соблюдают правила установленные компанией в трудовом договоре (их 86 %).
Мультиколлеанарности между признаками не замечено;
В ходе анализа Phik матрицы корреляций были определены силы связи целевого признака job_satisfaction_rate со входными, используя шкалу Чеддока:
- Высокая связь целевого признака прослеживается с входным признаком
supervisor_evaluation(0.76); - Средняя связь целевого признака прослеживается с входным признаком
last_year_violations(0.56): - Слабая связь целевого признака прослеживается с входным признаком
employment_years(0.33); - С остальными входными признаками связь очень слабая.
Для тестовой выборки
Распределения количественных значений датафрейма test_full отличаются от Гауссовсского;
Для
salary(ежемесячная заработная плата) наиболее популярны значения от 20000 до 30000. Так же были замечены выбросы с зарплатами более 73800 для отдельных сеньеров. Среднее значениеsalary-mean = 34067, медианное значениеsalary-median = 30000;Для
job_satisfaction_rate(уровень удовлетворённости сотрудника работой в компании) наиболее популярны значения от 0.6 до 0.8. Среднее значениеjob_satisfaction_rate-mean = 0.549, медианное значениеjob_satisfaction_rate-median = 0.58.Для
supervisor_evaluation(оценка качества работы сотрудника) наиболее популярные оценки 3 и 4. Среднее значениеsupervisor_evaluation-mean = 3.526, медианное значениеsupervisor_evaluation-median = 4. Так же в ходе анализа было найдено отсутствие зависимости качество работы сотрудника от занимаемой им должности;Для
employment_years(длительности работы в компании) наблюдается, что наибольшее количество работников проработали в компании 1 год, в ней также отсутствуют работники без опыта, в целом заметен ниспадающий тренд чем больше стаж тем меньше работников данного стажа в компании. Среднее значениеemployment_years-mean = 3.666, медианное значениеemployment_years-median = 3.Больше всего сотрудников в компании работает в продажах (38.1%), наименьшее число работает в hr;
В компании малое количество высококвалифицированных сотрудников (senior = 8.8%), сотрудников низкой и средней квалификации примерно равное количество (junior = 48.7%, middle = 42.7%);
Большинство сотрудников в компании имеют среднюю загруженность (их 52.1 %), наименьшее количество высокозагруженных работников (их 18.1%);
В компании редко происходят повышения, за прошлый год повысили только 3.1 % сотрудников;
Так же большинство сотрудников соблюдают правила установленные компанией в трудовом договоре (их 86.9%).
Мультиколлеанарности между признаками не замечено;
В ходе анализа Phik матрицы корреляций были определены силы связи целевого признака job_satisfaction_rate со входными, используя шкалу Чеддока:
- Высокая связь целевого признака прослеживается с входным признаком
supervisor_evaluation(0.77); - Средняя связь целевого признака прослеживается с входным признаком
last_year_violations(0.55): - Слабая связь целевого признака прослеживается с входным признаком
employment_years(0.31) иlast_year_promo; - С остальными входными признаками связь очень слабая.
В тестовой выборке на целевой признак job_satisfaction_rate входной last_year_promo 0.34 по сравнению с тренировочной 0.19. В остальном данные схожи поэтому:
Отличие тренировочной и тестовой выборки не выявлено, данные можно использовать для МО
Подготовка данных¶
# Тренировочные данные
X_train_1 = train_job_satisfaction_rate.drop(['job_satisfaction_rate', 'id'], axis=1)
y_train_1 = train_job_satisfaction_rate['job_satisfaction_rate']
# Тестовые данные
X_test_1 = test_full.drop(['job_satisfaction_rate', 'id'], axis=1)
y_test_1 = test_full['job_satisfaction_rate']
# Определение числовых и текстовых признаков
num_columns = X_train_1.select_dtypes(include=['int64', 'float64']).columns.tolist()
ohe_columns = X_train_1.select_dtypes(include=['object']).columns.tolist()
ohe_columns = [col for col in ohe_columns if col not in ['level', 'workload']]
ord_columns = ['level', 'workload']
display(ohe_columns)
['dept', 'last_year_promo', 'last_year_violations']
Вывод по подготовке данных:
Данные были разделены на тренировочную и тестовую выборку и подготовлены для дальнейшего обучения.
Обучение модели¶
Перечислим особенности данных:
- Три признака:
dept,last_year_promo,last_year_violations— нужно кодировать с помощью OneHotEncoder. - Два признака:
level,workload— нужно кодировать с помощью OrdinalEncoder. - Три Количественных признака:
employment_years,supervisor_evaluation,salary- нужно масштабировать. - В признаках пропуски встречаются и обработаем их в пайплайне.
- Целевой признак —
job_satisfaction_rate.
# создаём пайплайн для подготовки признаков из списка ohe_columns: заполнение пропусков и OHE-кодирование
# SimpleImputer + OHE
ohe_pipe = Pipeline(
[
(
'simpleImputer_ohe',
SimpleImputer(missing_values=np.nan, strategy='most_frequent')
),
(
'ohe',
OneHotEncoder(drop='first', handle_unknown='ignore', sparse_output=False)
)
]
)
ord_pipe = Pipeline(
[
(
'simpleImputer_before_ord',
SimpleImputer(missing_values=np.nan, strategy='most_frequent')
),
(
'ord',
OrdinalEncoder(
categories=[
['junior', 'middle', 'sinior'],
['low', 'medium', 'high']
],
handle_unknown='use_encoded_value', unknown_value=np.nan
)
),
(
'simpleImputer_after_ord',
SimpleImputer(missing_values=np.nan, strategy='most_frequent')
)
]
)
# создаём общий пайплайн для подготовки данных
data_preprocessor = ColumnTransformer(
[
('ohe', ohe_pipe, ohe_columns),
('ord', ord_pipe, ord_columns),
('num', MinMaxScaler(), num_columns)
],
remainder='passthrough'
)
pipe_final = Pipeline(
[
('preprocessor', data_preprocessor),
('model', DecisionTreeRegressor(random_state=RANDOM_STATE))
]
)
# Сетка гиперпараметров
param_grid = [
# Сетка для DecisionTreeRegressor
{
'model': [DecisionTreeRegressor(random_state=RANDOM_STATE)],
'model__max_depth': range(2, 15),
'model__max_features': range(2, len(num_columns) + len(ohe_columns) + len(ord_columns)), # Количество признаков
'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough'],
},
# Сетка для SVR
{
'model': [SVR()],
'model__C': [0.1, 1, 10],
'model__gamma': ['scale', 'auto', 0.1, 1],
'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough']
},
# Сетка для LinearRegression
{
'model': [LinearRegression()],
'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough']
},
# DummyRegressor
{
'model': [DummyRegressor()],
'model__strategy': ['mean']
}
]
def smape(y_true, y_pred):
"""
Вычисляет симметричную среднюю абсолютную процентную ошибку (SMAPE).
Формула:
SMAPE = (100 / N) * Σ(2 * |y_pred - y_true| / (|y_true| + |y_pred|))
Параметры:
----------
y_true : array
Истинные значения целевой переменной.
y_pred : array
Предсказанные значения целевой переменной.
Возвращает:
----------
float
Значение SMAPE (в процентах).:"""
return 100/len(y_true) * np.sum(2 * np.abs(y_pred - y_true) / (np.abs(y_true) + np.abs(y_pred)))
smape_scorer = make_scorer(score_func=smape, greater_is_better=False)
grid_search = GridSearchCV(
pipe_final,
param_grid,
n_jobs=-1,
cv=5,
scoring=smape_scorer
)
grid_search.fit(X_train_1, y_train_1)
GridSearchCV(cv=5,
estimator=Pipeline(steps=[('preprocessor',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('simpleImputer_ohe',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore',
sparse_output=False))]),
['dept',
'last_year_promo',
'last_year_violations']),
('ord',
Pipeline(...
{'model': [SVR()], 'model__C': [0.1, 1, 10],
'model__gamma': ['scale', 'auto', 0.1, 1],
'preprocessor__num': [StandardScaler(),
MinMaxScaler(),
'passthrough']},
{'model': [LinearRegression()],
'preprocessor__num': [StandardScaler(),
MinMaxScaler(),
'passthrough']},
{'model': [DummyRegressor()],
'model__strategy': ['mean']}],
scoring=make_scorer(smape, greater_is_better=False, response_method='predict'))In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
GridSearchCV(cv=5,
estimator=Pipeline(steps=[('preprocessor',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('simpleImputer_ohe',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore',
sparse_output=False))]),
['dept',
'last_year_promo',
'last_year_violations']),
('ord',
Pipeline(...
{'model': [SVR()], 'model__C': [0.1, 1, 10],
'model__gamma': ['scale', 'auto', 0.1, 1],
'preprocessor__num': [StandardScaler(),
MinMaxScaler(),
'passthrough']},
{'model': [LinearRegression()],
'preprocessor__num': [StandardScaler(),
MinMaxScaler(),
'passthrough']},
{'model': [DummyRegressor()],
'model__strategy': ['mean']}],
scoring=make_scorer(smape, greater_is_better=False, response_method='predict'))Pipeline(steps=[('preprocessor',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('simpleImputer_ohe',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore',
sparse_output=False))]),
['dept', 'last_year_promo',
'last_year_violations']),
('ord',
Pipeline(steps=[('simpleImputer_befor...
SimpleImputer(strategy='most_frequent')),
('ord',
OrdinalEncoder(categories=[['junior',
'middle',
'sinior'],
['low',
'medium',
'high']],
handle_unknown='use_encoded_value',
unknown_value=nan)),
('simpleImputer_after_ord',
SimpleImputer(strategy='most_frequent'))]),
['level', 'workload']),
('num', StandardScaler(),
['employment_years',
'supervisor_evaluation',
'salary'])])),
('model', SVR(C=1))])ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('simpleImputer_ohe',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore',
sparse_output=False))]),
['dept', 'last_year_promo',
'last_year_violations']),
('ord',
Pipeline(steps=[('simpleImputer_before_ord',
SimpleImputer(strategy='most_frequent')),
('ord',
OrdinalEncoder(categories=[['junior',
'middle',
'sinior'],
['low',
'medium',
'high']],
handle_unknown='use_encoded_value',
unknown_value=nan)),
('simpleImputer_after_ord',
SimpleImputer(strategy='most_frequent'))]),
['level', 'workload']),
('num', StandardScaler(),
['employment_years', 'supervisor_evaluation',
'salary'])])['dept', 'last_year_promo', 'last_year_violations']
SimpleImputer(strategy='most_frequent')
OneHotEncoder(drop='first', handle_unknown='ignore', sparse_output=False)
['level', 'workload']
SimpleImputer(strategy='most_frequent')
OrdinalEncoder(categories=[['junior', 'middle', 'sinior'],
['low', 'medium', 'high']],
handle_unknown='use_encoded_value', unknown_value=nan)SimpleImputer(strategy='most_frequent')
['employment_years', 'supervisor_evaluation', 'salary']
StandardScaler()
[]
passthrough
SVR(C=1)
best_model = grid_search.best_estimator_
print('Лучшая модель и её параметры:\n\n', best_model)
print ('Метрика лучшей модели на тренировочной выборке с использованием кросс-валидации:', grid_search.best_score_*(-1))
Лучшая модель и её параметры:
Pipeline(steps=[('preprocessor',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('simpleImputer_ohe',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore',
sparse_output=False))]),
['dept', 'last_year_promo',
'last_year_violations']),
('ord',
Pipeline(steps=[('simpleImputer_befor...
SimpleImputer(strategy='most_frequent')),
('ord',
OrdinalEncoder(categories=[['junior',
'middle',
'sinior'],
['low',
'medium',
'high']],
handle_unknown='use_encoded_value',
unknown_value=nan)),
('simpleImputer_after_ord',
SimpleImputer(strategy='most_frequent'))]),
['level', 'workload']),
('num', StandardScaler(),
['employment_years',
'supervisor_evaluation',
'salary'])])),
('model', SVR(C=1))])
Метрика лучшей модели на тренировочной выборке с использованием кросс-валидации: 15.058363611125369
print(f'Метрика SMAPE лучшей модели на тестовой выборке: {round(smape(y_test_1, grid_search.best_estimator_.predict(X_test_1)), 4)}')
Метрика SMAPE лучшей модели на тестовой выборке: 14.014
Вывод:
Лучшей моделью (подходящей условию SMAPE < 15 для тестовой выборки) является - SVR C=1 и ядром rbf, количественные данные которой были закодированы StandardScaler(), а категориальные OneHotEncoder() и OrdinalEncoder();
Значение SMAPE на тренировочной выборке с применением кросс-валидации равно ~ 15.05;
Значение SMAPE на тестовой выборке равно ~ 14.01.
Оформление выводов¶
Определение влияния входных признаков на модель
# Выборка небольшого подмножества данных для ускорения
X_train_sample = shap.sample(X_train_1, 500)
X_test_sample = shap.sample(X_test_1, 500)
# Преобразование данных через preprocessor для ускорения
X_train_transformed = grid_search.best_estimator_['preprocessor'].fit_transform(X_train_sample)
X_test_transformed = grid_search.best_estimator_['preprocessor'].transform(X_test_sample)
# Получаем имена признаков после трансформации
feature_names = grid_search.best_estimator_['preprocessor'].get_feature_names_out()
# Создаем DataFrame для удобства анализа
X_test_enc = pd.DataFrame(X_test_transformed, columns=feature_names)
# Создание SHAP Explainer для модели SVR
explainer = shap.PermutationExplainer(
model=grid_search.best_estimator_['model'].predict, # Предсказания через модель
data=X_train_transformed, # Преобразованные данные для обучения
masker=shap.maskers.Independent(data=X_train_transformed) # Указываем masker
)
# Расчёт SHAP-значений для тестового набора
shap_values = explainer.shap_values(X_test_transformed)
# Преобразуем SHAP-значения в Explanation объект
shap_values = shap.Explanation(
values=shap_values,
feature_names=feature_names, # Имена признаков
data=X_test_transformed # Исходные данные
)
PermutationExplainer explainer: 501it [02:13, 3.53it/s]
# Визуализация важности признаков с подписями осей
fig, ax = plt.subplots(figsize=(10, 6)) # Создаём фигуру и ось
shap.plots.bar(shap_values, max_display=30, show=False)
# Добавляем подписи осей
ax.set_xlabel("Среднее абсолютное SHAP-значение", fontsize=12)
ax.set_ylabel("Входные признаки", fontsize=12)
ax.set_title("Важность признаков (SHAP)", fontsize=14, fontweight='bold')
plt.show() # Отображаем график
shap.plots.beeswarm(shap_values, max_display=30, show=False)
# Добавляем заголовок через `plt`
plt.title("Важность признаков (SHAP)", fontsize=14, fontweight='bold')
plt.xlabel("SHAP-значение (влияние на модель)", fontsize=12)
plt.ylabel("Входные признаки", fontsize=12)
plt.show() # Отображаем график
Для поиска лучшей модели был использован Pipeline содержащий следующие шаги:
- тренировочные и тестовые входные данные были закодированы с помощью:
StandardScaler,OneHotEncoder,OrdinalEncoder; - в процессе поиска к данным применено 3 типа моделей регрессии:
DecisionTreeRegressor,SVR,LinearRegressionиDummyRegressor(); - на основе метрики
SMAPEбыла отобрана лучшая модель -SVRс гиперпараметрами (C=1 и ядромrbf); - значение SMAPE на тренировочной выборке с применением кросс-валидации равно ~ 15.51;
- значение SMAPE на тестовой выборке равно ~ 14.05.
Так же были определены наиболее влияющие входные признаки на целевой job_satisfaction_rate - ими являются: salary, supervisor_evaluation, level и workload.
Модель SVR справилась лучше, чем LinearRegression, т.к. входной признак salary сильно влияет на целевой признак, который связан с целевым нелинейно, это доказывает очень слабая линейная связь на тестовой выборке (коэф. корреляции = 0.17);
Модель DecisionTreeRegressor склонна к переобучению, особенно на малых данных или данных с выбросами (т.к. salary). Это может привести к резким скачкам в предсказаниях.
Задача 2: предсказание увольнения сотрудника из компании¶
Загрузка данных¶
try:
train_quit = pd.read_csv("C:\\Data-science\\ds_csv\\train_quit.csv")
test_target_quit = pd.read_csv("C:\\Data-science\\ds_csv\\test_target_quit.csv")
except:
try:
train_quit = pd.read_csv('/datasets/train_quit.csv')
test_target_quit = pd.read_csv('/datasets/test_target_quit.csv')
except:
raise FileNotFoundError
Изучение загруженных датасетов¶
# Создаем словарь, чтобы перебрать все импортируемые датафреймы
dataframes = {
"train_quit": train_quit,
"test_target_quit": test_target_quit
}
# Цикл по каждому DataFrame с выводом имени, информации и первых строк
for name, data in dataframes.items():
print(f'\033[1mНаименование анализируемого датафрейма:\033[0m {name}')
print()
data.info() # Выводим информацию о DataFrame
display(data.head(5)) # Отображаем первые 5 строк
print('=' * TERM_SIZE.columns) # Линия-разделитель по ширине терминала
Наименование анализируемого датафрейма: train_quit
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4000 entries, 0 to 3999
Data columns (total 10 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 4000 non-null int64
1 dept 4000 non-null object
2 level 4000 non-null object
3 workload 4000 non-null object
4 employment_years 4000 non-null int64
5 last_year_promo 4000 non-null object
6 last_year_violations 4000 non-null object
7 supervisor_evaluation 4000 non-null int64
8 salary 4000 non-null int64
9 quit 4000 non-null object
dtypes: int64(4), object(6)
memory usage: 312.6+ KB
| id | dept | level | workload | employment_years | last_year_promo | last_year_violations | supervisor_evaluation | salary | quit | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 723290 | sales | middle | high | 2 | no | no | 4 | 54000 | no |
| 1 | 814010 | sales | junior | medium | 2 | no | no | 4 | 27600 | no |
| 2 | 155091 | purchasing | middle | medium | 5 | no | no | 1 | 37200 | no |
| 3 | 257132 | sales | junior | medium | 2 | no | yes | 3 | 24000 | yes |
| 4 | 910140 | marketing | junior | medium | 2 | no | no | 5 | 25200 | no |
===============================================================================================================
Наименование анализируемого датафрейма: test_target_quit
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2000 entries, 0 to 1999
Data columns (total 2 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 id 2000 non-null int64
1 quit 2000 non-null object
dtypes: int64(1), object(1)
memory usage: 31.4+ KB
| id | quit | |
|---|---|---|
| 0 | 999029 | yes |
| 1 | 372846 | no |
| 2 | 726767 | no |
| 3 | 490105 | no |
| 4 | 416898 | yes |
===============================================================================================================
Определение наличия и количества пропусков¶
# Цикл по каждому DataFrame с выводом имени, информации и первых строк
for name, data in dataframes.items():
print(f'\033[1mНаименование анализируемого датафрейма:\033[0m {name}')
display(data.isna().sum()) # Отображаем первые 5 строк
print('=' * TERM_SIZE.columns) # Линия-разделитель по ширине терминала
Наименование анализируемого датафрейма: train_quit
id 0 dept 0 level 0 workload 0 employment_years 0 last_year_promo 0 last_year_violations 0 supervisor_evaluation 0 salary 0 quit 0 dtype: int64
===============================================================================================================
Наименование анализируемого датафрейма: test_target_quit
id 0 quit 0 dtype: int64
===============================================================================================================
Вывод по предварительному анализу:
В ходе предварительного анализа данных было выявлено:
- В датафреймах отсутствуют пропуски;
- Название столбцов имеют форму snake_case.
Предобработка данных¶
Проверка датафреймов на явные дубликаты¶
for name, data in dataframes.items():
print(f"Явных дубликатов в датасете {name} - {data.duplicated().sum()}: "
if data.duplicated().sum() > 0 else f"Явных дубликатов в датасете {name} - НЕТ")
Явных дубликатов в датасете train_quit - НЕТ Явных дубликатов в датасете test_target_quit - НЕТ
Проверка датафреймов на неявные дубликаты¶
for name, data in dataframes.items():
col_cat = data.select_dtypes(include=['object']).columns.to_list()
print(f'\033[1mНазвание датафрейма {name}\033[0m')
for col in col_cat:
print(f"Уникальные значения в столбце '{col}': {data[col].unique()}")
print('=' * TERM_SIZE.columns)
Название датафрейма train_quit Уникальные значения в столбце 'dept': ['sales' 'purchasing' 'marketing' 'technology' 'hr'] Уникальные значения в столбце 'level': ['middle' 'junior' 'sinior'] Уникальные значения в столбце 'workload': ['high' 'medium' 'low'] Уникальные значения в столбце 'last_year_promo': ['no' 'yes'] Уникальные значения в столбце 'last_year_violations': ['no' 'yes'] Уникальные значения в столбце 'quit': ['no' 'yes'] =============================================================================================================== Название датафрейма test_target_quit Уникальные значения в столбце 'quit': ['yes' 'no'] ===============================================================================================================
train_quit['level'] = train_quit['level'].replace('sinior', 'senior')
Вывод по предобработке данных:
В ходе предобработке данных была выполненна проверка датафреймов на дубликаты, в результате которой дубликатов необнаружено, а опечатка в должности senior в датафрейме train_quit исправлена.
Исследовательский анализ данных¶
Объединение тестовой выборки для статистического анализа¶
test_quit_full = test_features.merge(test_target_quit, on='id', how='inner')
display(test_quit_full.shape)
test_quit_full.sample(5)
(2000, 10)
| id | dept | level | workload | employment_years | last_year_promo | last_year_violations | supervisor_evaluation | salary | quit | |
|---|---|---|---|---|---|---|---|---|---|---|
| 820 | 906310 | sales | senior | high | 5 | no | no | 5 | 78000 | no |
| 205 | 508688 | marketing | junior | low | 1 | no | no | 3 | 16800 | yes |
| 37 | 484655 | hr | junior | low | 4 | no | no | 4 | 16800 | no |
| 1574 | 861257 | sales | junior | medium | 2 | no | no | 4 | 25200 | no |
| 1114 | 417411 | sales | junior | low | 2 | no | no | 4 | 12000 | yes |
Анализ количественных и качественных признаков¶
Для тренировочных данных train_quit¶
# Формирование списка столбцов с количественными признаками
num_variables_col = train_quit.select_dtypes(include=['number']).columns.to_list()
num_variables_col = [col for col in num_variables_col if col not in ['id', 'supervisor_evaluation', 'employment_years']]
num_variables_col
['salary']
# Вывод графиков для датафрейма train_quit
for col in num_variables_col:
show_num_variable(train_quit, col, 'quit')
normal_check(train_quit, col)
print('=' * TERM_SIZE.columns)
Выдвенем гипотезы:
- Н0: Распределение параметра salary является нормальным.
- Н1: Распределение параметра salary не является нормальным.
Тест Шапиро — Уилка: Stat=0.930, p=1.09e-39
Распределение данных не нормальное с вероятностью более 0.95. Отвергаем нулевую гипотезу
===============================================================================================================
train_quit.groupby('quit')[num_variables_col].describe().round(3).T
| quit | no | yes | |
|---|---|---|---|
| salary | count | 2872.000 | 1128.000 |
| mean | 37702.228 | 23885.106 | |
| std | 15218.977 | 9351.600 | |
| min | 12000.000 | 12000.000 | |
| 25% | 25200.000 | 16800.000 | |
| 50% | 34800.000 | 22800.000 | |
| 75% | 46800.000 | 27600.000 | |
| max | 96000.000 | 79200.000 |
Вывод для количесвенных значений датафрейма train_quit:
- Распределения количественных значений датафрейма test_quit отличаются от Гауссовсского;
- Плотность распределения зарплат сотрудников зависит от целевого признака
quit; - Для уволившихся сотрудников
quit = yesзаметна более низкая сумма зп, нежели у тех сотрудников кто остался. - Среднее значение
salaryдляquit = yes:mean = 23885, медианное значениеmedian = 22800; - Среднее значение
salaryдляquit = no:mean = 37702, медианное значениеmedian = 34800.
Есть предположение, что данная зависимость связанна с тем, что чаще увольняются сотрудники с низких должностей, что сильно влияет на анализ. Проверим влияет ли должность сотрудника при увольнении на зарплату.
suptitle = f'Зависимость salary от занимаемо должности для уволившихся сотрудников'
show_num_variable(
train_quit.query('quit == "yes"'),
'salary',
'level',
suptitle
)
train_quit.query('quit == "yes"').groupby('level')['salary'].describe().round(3).T
| level | junior | middle | senior |
|---|---|---|---|
| count | 1003.000 | 108.000 | 17.000 |
| mean | 22508.076 | 33122.222 | 46447.059 |
| std | 7583.086 | 11971.862 | 19095.488 |
| min | 12000.000 | 18000.000 | 25200.000 |
| 25% | 15600.000 | 22800.000 | 31200.000 |
| 50% | 21600.000 | 28800.000 | 45600.000 |
| 75% | 27600.000 | 45600.000 | 60000.000 |
| max | 48000.000 | 62400.000 | 79200.000 |
suptitle = f'Зависимость salary от занимаемо должности для оставшихся сотрудников'
show_num_variable(
train_quit.query('quit == "no"'),
'salary',
'level',
suptitle
)
train_quit.query('quit == "no"').groupby('level')['salary'].describe().round(3).T
| level | junior | middle | senior |
|---|---|---|---|
| count | 946.000 | 1586.000 | 340.000 |
| mean | 25661.734 | 40075.914 | 60130.588 |
| std | 5991.478 | 12101.534 | 15535.602 |
| min | 12000.000 | 18000.000 | 25200.000 |
| 25% | 21600.000 | 32400.000 | 50400.000 |
| 50% | 26400.000 | 39600.000 | 58800.000 |
| 75% | 28800.000 | 48000.000 | 73200.000 |
| max | 48000.000 | 70800.000 | 96000.000 |
В результате получили:
В среднем у уволившихся сотрудников заработная плата меньше, чем у оставшихся независимо от его должности.
# Формирование списка столбцов с количественными дискретными признаками
discrete_variables_col = ['supervisor_evaluation', 'employment_years']
discrete_variables_col
['supervisor_evaluation', 'employment_years']
# Вывод графиков для дискретных значений датафрейма
# train_quit в зависимости от целевого признака quit
for col in discrete_variables_col:
show_discrete_variable(train_quit, col, 'quit')
print('=' * TERM_SIZE.columns)
===============================================================================================================
===============================================================================================================
train_quit.groupby('quit')[discrete_variables_col].describe().round(3).T
| quit | no | yes | |
|---|---|---|---|
| supervisor_evaluation | count | 2872.000 | 1128.000 |
| mean | 3.643 | 3.046 | |
| std | 0.965 | 0.973 | |
| min | 1.000 | 1.000 | |
| 25% | 3.000 | 3.000 | |
| 50% | 4.000 | 3.000 | |
| 75% | 4.000 | 4.000 | |
| max | 5.000 | 5.000 | |
| employment_years | count | 2872.000 | 1128.000 |
| mean | 4.431 | 1.845 | |
| std | 2.545 | 1.275 | |
| min | 1.000 | 1.000 | |
| 25% | 2.000 | 1.000 | |
| 50% | 4.000 | 1.000 | |
| 75% | 6.000 | 2.000 | |
| max | 10.000 | 10.000 |
Вывод для дискретных признаков в датафрейма train_quit:
Для уволившихся сотрудников (quit = yes):
- Для признака
supervisor_evaluation(оценка качества работы сотрудника) наиболее популярная оценка 3 (46.4 % от всех уволившихся сотрудников). Среднее значениеsupervisor_evaluation-mean = 3.046, медианное значениеsupervisor_evaluation-median = 3; - Для признака
employment_years(длительности работы в компании) наблюдается, что наибольшее количество уволившихся работников проработали в компании 1 год (53.1 %). Среднее значениеemployment_years-mean = 1.845, медианное значениеemployment_years-median = 1.
Для оставшихся сотрудников (quit = no):
- Для признака
supervisor_evaluation(оценка качества работы сотрудника) наиболее популярная оценка 4 (47.6 % от всех оставшихся сотрудников). Среднее значениеsupervisor_evaluation-mean = 3.643, медианное значениеsupervisor_evaluation-median = 4; - Для признака
employment_years(длительности работы в компании) примерно одинаковое соотношение оставшихся работников, есть небольшое преобладание сотрудников имеющих стаж 2 года (17 %) и постепенное уменьшение количества сотрудников начиная с 8 лет и более. Среднее значениеemployment_years-mean = 4.431, медианное значениеemployment_years-median = 4.
Наблюдается явная зависимость входных признаков supervisor_evaluation и employment_years от целевого quit.
У уволившихся сотрудников маленький стаж и хуже отношение с руководством.
# Лист с названиями столбцов категориальных признаков
dict_market_file_cat = train_quit.select_dtypes(include=['object']).columns.to_list()
for col in dict_market_file_cat:
show_cat_variable_by_target(train_quit, col, col)
print('=' * TERM_SIZE.columns)
===============================================================================================================
===============================================================================================================
===============================================================================================================
===============================================================================================================
===============================================================================================================
===============================================================================================================
Вывод по категориальным данным датафрейма train_quit:
Данные по сотрудникам датафреймов train_job_satisfaction_rate и train_quit схожи
Наблюдается дисбаланс целевого признака quit. В компании за 1 год уволилось 1/3 сотрудников (28.2 %)
- Больше всего сотрудников в компании работает в продажах (36 %), наименьшее число работает в hr;
- В компании малое количество высококвалифицированных сотрудников (senior = 8.9 %), сотрудников низкой и средней квалификации примерно равное количество (junior = 48.7 %, middle = 42.4 %);
- Большинство сотрудников в компании имеют среднюю загруженность (их 53 %), наименьшее количество высокозагруженных работников (их 16.9 %);
- В компании редко происходят повышения, за прошлый год повысили 2.8 % сотрудников;
- Так же большинство сотрудников соблюдают правила установленные компанией в трудовом договоре (их 86.4 %).
Иследование категориальных данных датафрейма train_quit в зависимости от значения целевого признака quit¶
# Лист с названиями столбцов категориальных признаков
list_cat = train_quit.select_dtypes(include=['object']).columns.to_list()
list_cat.remove('quit')
for col in list_cat:
show_cat_variable_by_target(train_quit, col, col, 'quit')
print('=' * TERM_SIZE.columns)
===============================================================================================================
===============================================================================================================
===============================================================================================================
===============================================================================================================
===============================================================================================================
Вывод по анализу графиков для датафрейма train_quit
Целевой признак quit (уход из компании) разделен на две категории "no" 71.8 % и "yes" - 28.2 %;
Распределение целевого признак quit имеет дисбаланс;
Целевой признак quit влияет на некоторые категориальные признаки:
- Входной признак
levelдля целевого признака:quit-noимеет распределение junior - 55.2 %, middle - 32.9 % и senior - 11.8 %;quit-yesимеет распределение junior - 88.9 %, middle - 9.6 % и senior - 1.5 %;
Это говорит о том, что чаще всего увольняются сотрудники с малым опытом работы;
- Входной признак
workload(уровень загруженности сотрудников) для целевого признака:quit-noимеет распределение low - 56.8 %, medium - 24 % и high - 19.3 %;quit-yesимеет распределение low - 46 %, medium - 43.3 % и high - 10.7 %;
У сотрудников которые увольняются выше нагруженность;
- Входной признак
last_year_promo(было ли повышение за последний год) для целевого признака:quit-noимеет распределение no - 96.1 %, yes - 3.9 %;quit-yesимеет распределение no - 99.9 %, yes - 0.1 %;
В компании практически не повышают сотрудников, а сотрудники которые увольняются вообще не повышают;
- Входной признак
last_year_violations(нарушал ли сотрудник трудовой договор за последний год) для целевого признака:quit-noимеет распределение no - 89 %, yes - 11 %;quit-yesимеет распределение no - 79.8 %, yes - 20.2 %;
В компании чаще нарушают правила сотрудники которые увольняются.
Корреляционный анализ¶
Для корреляционного анализа и впоследствии модели машинного обучения, признак id не нужен, т.к. не несет никакой смысловой нагрузки, а так же он может повлиять на итоговую модель.
# Удаление столбца id
train_quit_without_id = train_quit.drop('id', axis=1)
col_names_corr = ['salary']
big_data_corr = train_quit_without_id.phik_matrix(interval_cols=col_names_corr)
plt.figure(figsize=(14, 10))
sns.heatmap(big_data_corr, annot=True, fmt=".2f", center=0)
plt.title('Матрица корреляций phik')
plt.show()
Вывод по корреляционному анализу
Мультиколлеанарности между признаками не замечено;
В ходе анализа Phik матрицы корреляций были определены силы связи целевого признака quit со входными, используя шкалу Чеддока:
- Средняя связь целевого признака прослеживается с входным признаком
employment_years(0.66) иsalary(0.56): - Слабая связь целевого признака прослеживается с входным признаком
level(0.31); - С остальными входными признаками связь очень слабая.
Для тестовых данных test_quit_full¶
# Формирование списка столбцов с количественными признаками
num_variables_col = test_quit_full.select_dtypes(include=['number']).columns.to_list()
num_variables_col = [col for col in num_variables_col if col not in ['id', 'supervisor_evaluation', 'employment_years']]
num_variables_col
['salary']
# Вывод графиков для датафрейма test_quit_full
for col in num_variables_col:
show_num_variable(test_quit_full, col, 'quit')
normal_check(test_quit_full, col)
print('=' * TERM_SIZE.columns)
Выдвенем гипотезы:
- Н0: Распределение параметра salary является нормальным.
- Н1: Распределение параметра salary не является нормальным.
Тест Шапиро — Уилка: Stat=0.926, p=2.13e-30
Распределение данных не нормальное с вероятностью более 0.95. Отвергаем нулевую гипотезу
===============================================================================================================
test_quit_full.groupby('quit')[num_variables_col].describe().round(3).T
| quit | no | yes | |
|---|---|---|---|
| salary | count | 1436.000 | 564.000 |
| mean | 37645.404 | 24955.319 | |
| std | 15503.475 | 10650.301 | |
| min | 12000.000 | 12000.000 | |
| 25% | 25200.000 | 18000.000 | |
| 50% | 33600.000 | 22800.000 | |
| 75% | 48000.000 | 30000.000 | |
| max | 96000.000 | 80400.000 |
Аналогично тренировочной выборке проверим зависит ли уход сотрудника от зп на занимаемой должности
suptitle = f'Зависимость salary от занимаемо должности для уволившихся сотрудников'
show_num_variable(
test_quit_full.query('quit == "yes"'),
'salary',
'level',
suptitle
)
test_quit_full.query('quit == "yes"').groupby('level')['salary'].describe().round(3).T
| level | junior | middle | senior |
|---|---|---|---|
| count | 488.000 | 62.000 | 13.000 |
| mean | 22790.164 | 35477.419 | 57046.154 |
| std | 7458.990 | 13061.788 | 17442.124 |
| min | 12000.000 | 18000.000 | 31200.000 |
| 25% | 15600.000 | 21900.000 | 44400.000 |
| 50% | 21600.000 | 33600.000 | 57600.000 |
| 75% | 27600.000 | 46800.000 | 72000.000 |
| max | 48000.000 | 66000.000 | 80400.000 |
suptitle = f'Зависимость salary от занимаемо должности для оставшихся сотрудников'
show_num_variable(
test_quit_full.query('quit == "no"'),
'salary',
'level',
suptitle
)
test_quit_full.query('quit == "no"').groupby('level')['salary'].describe().round(3).T
| level | junior | middle | senior |
|---|---|---|---|
| count | 486.000 | 792.000 | 158.000 |
| mean | 25866.667 | 40098.485 | 61579.747 |
| std | 6204.594 | 12479.692 | 16030.658 |
| min | 12000.000 | 18000.000 | 25200.000 |
| 25% | 21600.000 | 30000.000 | 51600.000 |
| 50% | 26400.000 | 39600.000 | 61200.000 |
| 75% | 30000.000 | 50400.000 | 72000.000 |
| max | 48000.000 | 69600.000 | 96000.000 |
Аналогично тренировочной выборке, в среднем у уволившихся сотрудников заработная плата меньше, чем у оставшихся независимо от его должности.
# Вывод графиков для дискретных значений датафрейма
# test_quit_full в зависимости от целевого признака quit
for col in discrete_variables_col:
show_discrete_variable(test_quit_full, col, 'quit')
print('=' * TERM_SIZE.columns)
===============================================================================================================
===============================================================================================================
test_quit_full.groupby('quit')[discrete_variables_col].describe().round(3).T
| quit | no | yes | |
|---|---|---|---|
| supervisor_evaluation | count | 1436.000 | 564.000 |
| mean | 3.717 | 3.043 | |
| std | 0.959 | 0.926 | |
| min | 1.000 | 1.000 | |
| 25% | 3.000 | 3.000 | |
| 50% | 4.000 | 3.000 | |
| 75% | 4.000 | 4.000 | |
| max | 5.000 | 5.000 | |
| employment_years | count | 1436.000 | 564.000 |
| mean | 4.331 | 1.975 | |
| std | 2.541 | 1.553 | |
| min | 1.000 | 1.000 | |
| 25% | 2.000 | 1.000 | |
| 50% | 4.000 | 1.000 | |
| 75% | 6.000 | 2.000 | |
| max | 10.000 | 10.000 |
# Лист с названиями столбцов категориальных признаков
dict_market_file_cat = test_quit_full.select_dtypes(include=['object']).columns.to_list()
for col in dict_market_file_cat:
show_cat_variable_by_target(test_quit_full, col, col)
print('=' * TERM_SIZE.columns)
===============================================================================================================
===============================================================================================================
===============================================================================================================
===============================================================================================================
===============================================================================================================
===============================================================================================================
Иследование категориальных данных датафрейма test_quit_full в зависимости от значения целевого признака quit¶
# Лист с названиями столбцов категориальных признаков
list_cat = test_quit_full.select_dtypes(include=['object']).columns.to_list()
list_cat.remove('quit')
for col in list_cat:
show_cat_variable_by_target(test_quit_full, col, col, 'quit')
print('=' * TERM_SIZE.columns)
===============================================================================================================
===============================================================================================================
===============================================================================================================
===============================================================================================================
===============================================================================================================
Корреляционный анализ¶
# Удаление столбца id
test_quit_without_id = test_quit_full.drop('id', axis=1)
col_names_corr = ['salary']
big_data_corr = test_quit_without_id.phik_matrix(interval_cols=col_names_corr)
plt.figure(figsize=(14, 10))
sns.heatmap(big_data_corr, annot=True, fmt=".2f", center=0)
plt.title('Матрица корреляций phik')
plt.show()
Было замечено отличие в столбце workload для тестовой и тренировочной выборки
(в тренировочной у оставшихся сотрудников low - 56.8 %, medium - 24 %, high - 19.3 %, у уволившихся low - 46 %, medium - 43.3 %, high - 10,7 %;
в тестовой у оставшихся сотрудников low - 24.8 %, medium - 55.1 %, high - 20.1 %, у уволившихся low - 42 %, medium - 44.7 %, high - 13,3 %).
Данное отличие не существенно, т.к. не имеет сильной корреляции на целевой признак (в тренировочном датафрейме корреляция для workload = 0.13, в тестовой корреляция для workload = 0.1)
Существенных отличий тренировочной и тестовой выборки не выявлено, данные можно использовать для МО
Портрет уходящего работника¶
Сотрудник который увольняется работает недавно, у него зарплата ниже чем у коллег на тех же должностях, его не повышают, он чаще нарушает правила, а начальство хуже его к нему относится.
Визуализация и сравнение распределения признака job_satisfaction_rate для ушедших и оставшихся сотрудников¶
Аналитики утверждают, что уровень удовлетворённости сотрудника работой в компании влияет на то, уволится ли сотрудник. Проверим данное утверждение
test_quit_full = test_quit_full.merge(test_target_job_satisfaction_rate, on='id', how='inner')
display(test_quit_full.shape)
test_quit_full.head(5)
(2000, 11)
| id | dept | level | workload | employment_years | last_year_promo | last_year_violations | supervisor_evaluation | salary | quit | job_satisfaction_rate | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 485046 | marketing | junior | medium | 2 | no | no | 5 | 28800 | no | 0.79 |
| 1 | 686555 | hr | junior | medium | 1 | no | no | 4 | 30000 | no | 0.72 |
| 2 | 467458 | sales | middle | low | 5 | no | no | 4 | 19200 | no | 0.64 |
| 3 | 418655 | sales | middle | low | 6 | no | no | 4 | 19200 | no | 0.60 |
| 4 | 789145 | hr | middle | medium | 5 | no | no | 5 | 40800 | no | 0.75 |
show_num_variable(test_quit_full, 'job_satisfaction_rate', 'quit')
test_quit_full.groupby('quit')['job_satisfaction_rate'].describe().round(3).T
| quit | no | yes |
|---|---|---|
| count | 1436.000 | 564.000 |
| mean | 0.612 | 0.388 |
| std | 0.199 | 0.186 |
| min | 0.030 | 0.040 |
| 25% | 0.500 | 0.240 |
| 50% | 0.660 | 0.370 |
| 75% | 0.760 | 0.492 |
| max | 1.000 | 0.970 |
Вывод
Увольняющиеся сотрудники имеют более низкий уровень удовлетворённости работой в компании
Общий вывод по исследовательскому анализу
Количественные данные:
- Распределения количественных значений датафрейма test_quit отличаются от Гауссовсского;
- Плотность распределения зарплат сотрудников зависит от целевого признака
quit; - Для уволившихся сотрудников
quit = yesзаметна более низкая сумма зп, нежели у тех сотрудников кто остался. - Среднее значение
salaryдляquit = yes:mean = 23885, медианное значениеmedian = 22800; - Среднее значение
salaryдляquit = no:mean = 37702, медианное значениеmedian = 34800.
Дискретные данные:
Для уволившихся сотрудников (quit = yes):
- Для признака
supervisor_evaluation(оценка качества работы сотрудника) наиболее популярная оценка 3 (46.4 % от всех уволившихся сотрудников). Среднее значениеsupervisor_evaluation-mean = 3.046, медианное значениеsupervisor_evaluation-median = 3; - Для признака
employment_years(длительности работы в компании) наблюдается, что наибольшее количество уволившихся работников проработали в компании 1 год (53.1 %). Среднее значениеemployment_years-mean = 1.845, медианное значениеemployment_years-median = 1.
Для оставшихся сотрудников (quit = no):
- Для признака
supervisor_evaluation(оценка качества работы сотрудника) наиболее популярная оценка 4 (47.6 % от всех оставшихся сотрудников). Среднее значениеsupervisor_evaluation-mean = 3.643, медианное значениеsupervisor_evaluation-median = 4; - Для признака
employment_years(длительности работы в компании) примерно одинаковое соотношение оставшихся работников, есть небольшое преобладание сотрудников имеющих стаж 2 года (17 %) и постепенное уменьшение количества сотрудников начиная с 8 лет и более. Среднее значениеemployment_years-mean = 4.431, медианное значениеemployment_years-median = 4.
Наблюдается явная зависимость входных признаков supervisor_evaluation и employment_years от целевого quit.
Категориальные данные:
Данные по сотрудникам датафреймов train_job_satisfaction_rate и train_quit схожи
Наблюдается дисбаланс целевого признака quit. В компании за 1 год уволилось 1/3 сотрудников (28.2 %)
Обший анализ категориальных данных:
- Больше всего сотрудников в компании работает в продажах (36 %), наименьшее число работает в hr;
- В компании малое количество высококвалифицированных сотрудников (senior = 8.9 %), сотрудников низкой и средней квалификации примерно равное количество (junior = 48.7 %, middle = 42.4 %);
- Большинство сотрудников в компании имеют среднюю загруженность (их 53 %), наименьшее количество высокозагруженных работников (их 16.9 %);
- В компании редко происходят повышения, за прошлый год повысили 2.8 % сотрудников;
- Так же большинство сотрудников соблюдают правила установленные компанией в трудовом договоре (их 86.4 %).
Анализ категориальных данных в зависимости от целевого признака
quit:
Целевой признакquit(уход из компании) разделен на две категории "no" 71.8 % и "yes" - 28.2 %;
Распределение целевого признакquitимеет дисбаланс;
Целевой признакquitвлияет на некоторые категориальные признаки: - Входной признак
levelдля целевого признака:quit-noимеет распределение junior - 55.2 %, middle - 32.9 % и senior - 11.8 %;quit-yesимеет распределение junior - 88.9 %, middle - 9.6 % и senior - 1.5 %;
Это говорит о том, что чаще всего увольняются сотрудники с малым опытом работы;
- Входной признак
workload(уровень загруженности сотрудников) для целевого признака:quit-noимеет распределение low - 56.8 %, medium - 24 % и high - 19.3 %;quit-yesимеет распределение low - 46 %, medium - 43.3 % и high - 10.7 %;
У сотрудников которые увольняются выше нагруженность;
- Входной признак
last_year_promo(было ли повышение за последний год) для целевого признака:quit-noимеет распределение no - 96.1 %, yes - 3.9 %;quit-yesимеет распределение no - 99.9 %, yes - 0.1 %;
В компании практически не повышают сотрудников, а сотрудники которые увольняются вообще не повышают;
- Входной признак
last_year_violations(нарушал ли сотрудник трудовой договор за последний год) для целевого признака:quit-noимеет распределение no - 89 %, yes - 11 %;quit-yesимеет распределение no - 79.8 %, yes - 20.2 %;
В компании чаще нарушают правила сотрудники которые увольняются.
Существенных отличий тренировочной и тестовой выборки не выявлено, данные можно использовать для МО
Добавление нового входного признака¶
train_quit['job_satisfaction_rate'] = best_model.predict(train_quit)
train_quit.sample(5)
| id | dept | level | workload | employment_years | last_year_promo | last_year_violations | supervisor_evaluation | salary | quit | job_satisfaction_rate | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 3039 | 904468 | purchasing | junior | high | 1 | no | no | 5 | 33600 | yes | 0.612660 |
| 3852 | 947313 | technology | junior | medium | 1 | no | no | 1 | 30000 | yes | 0.301611 |
| 2495 | 327728 | hr | middle | medium | 9 | no | yes | 4 | 40800 | no | 0.636685 |
| 1572 | 151574 | sales | middle | high | 5 | no | no | 5 | 49200 | no | 0.689072 |
| 2023 | 365585 | purchasing | middle | high | 5 | no | no | 3 | 58800 | no | 0.600228 |
data_quit_full = pd.concat([train_quit, test_quit_full])
# Удаление столбца id
data_quit_full_without_id = data_quit_full.drop('id', axis=1)
col_names_corr = data_quit_full_without_id.select_dtypes(include='number').columns.to_list()
big_data_corr = data_quit_full_without_id.phik_matrix(interval_cols=col_names_corr)
plt.figure(figsize=(12, 10))
sns.heatmap(big_data_corr, annot=True, fmt=".2f", center=0)
plt.title('Матрица корреляций phik')
plt.show()
Вывод по добавлению нового входного признака:
После добавления нового признака мультиколлениарность не появилась, данные пригодны для обучения моделей.
Подготовка данных¶
# Тренировочные данные
X_train_2 = train_quit.drop(['quit', 'id'], axis=1)
y_train_2 = train_quit['quit']
# Тестовые данные
X_test_2 = test_quit_full.drop(['quit', 'id'], axis=1)
y_test_2 = test_quit_full['quit']
# Определение числовых и текстовых признаков
num_columns = X_train_2.select_dtypes(include=['int64', 'float64']).columns.tolist()
ohe_columns = X_train_2.select_dtypes(include=['object']).columns.tolist()
ohe_columns = [col for col in ohe_columns if col not in ['level', 'workload']]
ord_columns = ['level', 'workload']
# Кодировка целевого признака
le = LabelEncoder()
y_train_2 = le.fit_transform(y_train_2)
y_test_2 = le.transform(y_test_2)
Вывод по подготовке данных:
Данные были разделены на тренировочную и тестовую выборку и подготовлены для дальнейшего обучения.
Обучение модели¶
Перечислим особенности данных:
- Три признака:
dept,last_year_promo,last_year_violations— нужно кодировать с помощью OneHotEncoder. - Два признака:
level,workload— нужно кодировать с помощью OrdinalEncoder. - Количественных 4 признака:
employment_years,supervisor_evaluation,salary,job_satisfaction_rate- нужно масштабировать. - В признаках пропуски встречаются и обработаем их в пайплайне.
- Целевой признак —
quit. Задачу мультиклассовой классификации тут рассматривать не будем.
# создаём пайплайн для подготовки признаков из списка ohe_columns: заполнение пропусков и OHE-кодирование
# SimpleImputer + OHE
ohe_pipe = Pipeline(
[
(
'simpleImputer_ohe',
SimpleImputer(missing_values=np.nan, strategy='most_frequent')
),
(
'ohe',
OneHotEncoder(drop='first', handle_unknown='ignore', sparse_output=False)
)
]
)
ord_pipe = Pipeline(
[
(
'simpleImputer_before_ord',
SimpleImputer(missing_values=np.nan, strategy='most_frequent')
),
(
'ord',
OrdinalEncoder(
categories=[
['junior', 'middle', 'sinior'],
['low', 'medium', 'high']
],
handle_unknown='use_encoded_value', unknown_value=np.nan
)
),
(
'simpleImputer_after_ord',
SimpleImputer(missing_values=np.nan, strategy='most_frequent')
)
]
)
# создаём общий пайплайн для подготовки данных
data_preprocessor = ColumnTransformer(
[
('ohe', ohe_pipe, ohe_columns),
('ord', ord_pipe, ord_columns),
('num', MinMaxScaler(), num_columns)
],
remainder='passthrough'
)
pipe_final_2 = Pipeline(
[
('preprocessor', data_preprocessor),
('model', DecisionTreeClassifier(random_state=RANDOM_STATE))
]
)
param_grid = [
# словарь для модели KNeighborsClassifier()
{
# название модели
'model': [KNeighborsClassifier()],
# указываем гиперпараметр модели n_neighbors
'model__n_neighbors': range(1, 10),
# указываем список методов масштабирования
'preprocessor__num': [StandardScaler(), MinMaxScaler()]
},
# словарь для модели DecisionTreeClassifier()
{
'model': [DecisionTreeClassifier(random_state=RANDOM_STATE)],
'model__max_depth': range(2, 15),
'model__max_features': range(2, len(num_columns) + len(ohe_columns) + len(ord_columns)),
'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough'],
},
# словарь для модели SVC()
{
'model': [SVC(probability=True, random_state=RANDOM_STATE)],
'model__C': [0.1, 1, 10],
'model__gamma': ['scale', 'auto', 0.1, 1],
'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough']
},
# словарь для модели LogisticRegression()
{
'model': [LogisticRegression(solver='liblinear', penalty='l1', random_state=RANDOM_STATE)],
'model__C': [0.1, 1, 10],
'preprocessor__num': [StandardScaler(), MinMaxScaler(), 'passthrough']
},
# словарь для модели DummyClassifier()
{
'model': [DummyClassifier(random_state=RANDOM_STATE)],
'model__strategy': ['most_frequent']
}
]
grid_search_2 = GridSearchCV(
pipe_final_2,
param_grid=param_grid,
cv=5,
scoring='roc_auc',
n_jobs=-1
)
grid_search_2.fit(X_train_2, y_train_2)
print('Лучшая модель и её параметры:\n\n', grid_search_2.best_estimator_)
print ('Метрика лучшей модели на тренировочной выборке с использованием кросс-валидации:', round(grid_search_2.best_score_, 4))
Лучшая модель и её параметры:
Pipeline(steps=[('preprocessor',
ColumnTransformer(remainder='passthrough',
transformers=[('ohe',
Pipeline(steps=[('simpleImputer_ohe',
SimpleImputer(strategy='most_frequent')),
('ohe',
OneHotEncoder(drop='first',
handle_unknown='ignore',
sparse_output=False))]),
['dept', 'last_year_promo',
'last_year_violations']),
('ord',
Pipeline(steps=[('simpleImputer_befor...
'sinior'],
['low',
'medium',
'high']],
handle_unknown='use_encoded_value',
unknown_value=nan)),
('simpleImputer_after_ord',
SimpleImputer(strategy='most_frequent'))]),
['level', 'workload']),
('num', MinMaxScaler(),
['employment_years',
'supervisor_evaluation',
'salary',
'job_satisfaction_rate'])])),
('model',
SVC(C=10, gamma='auto', probability=True, random_state=42))])
Метрика лучшей модели на тренировочной выборке с использованием кросс-валидации: 0.9327
# Предсказание на тестовой выборке
y_pred_2 = grid_search_2.predict(X_test_2)
# Проверка на наличие метода predict_proba
if hasattr(grid_search_2.best_estimator_['model'], 'predict_proba'):
# Предсказание вероятностей классов
proba_new = grid_search_2.predict_proba(X_test_2)[:, 1]
# Вычисление ROC-AUC на вероятностях
roc_auc_new = roc_auc_score(y_test_2, proba_new)
print(f'Метрика ROC-AUC на тестовой выборке: {roc_auc_new:.4f}')
else:
print("Метод predict_proba не поддерживается для лучшей модели.")
Метрика ROC-AUC на тестовой выборке: 0.9289
Вывод:
Лучшей моделью (подходящей условию ROC-AUC > 0.91 для тестовой выборки) является - SVC(C=10, gamma='auto', probability=True, random_state=42 и ядром rbf), количественные данные которой были закодированы MinMaxScaler(), а категориальные OneHotEncoder() и OrdinalEncoder().
Значение метрики ROC-AUC на тренировочной выборке равно ~ 0.9327;
Значение метрики ROC-AUC на тестовой выборке равно ~ 0.9289.
Оформление выводов¶
X_train_sample = shap.sample(X_train_2, 250)
X_test_sample = shap.sample(X_test_2, 250)
# Преобразуем тренировочные данные
X_train_transformed = grid_search_2.best_estimator_['preprocessor'].fit_transform(X_train_sample)
# Преобразуем тренировочные данные
X_test_transformed = grid_search_2.best_estimator_['preprocessor'].transform(X_test_sample)
# Получаем имена признаков
feature_names = grid_search_2.best_estimator_['preprocessor'].get_feature_names_out()
# Создаем объяснитель SHAP с PermutationExplainer
explainer = shap.PermutationExplainer(
model=grid_search_2.best_estimator_['model'].predict_proba, # Предсказания через модель
data=X_train_transformed, # Преобразованные данные для обучения
masker=shap.maskers.Independent(data=X_train_transformed) # Указываем masker
)
# Преобразуем тестовые данные
X_test_enc = grid_search_2.best_estimator_['preprocessor'].transform(X_test_sample)
# Создаем DataFrame для удобства анализа
X_test_enc = pd.DataFrame(X_test_enc, columns=feature_names)
# Вычисляем SHAP-значения
shap_values = explainer.shap_values(X_test_enc)
PermutationExplainer explainer: 251it [01:58, 1.95it/s]
shap_values_class_1 = shap.Explanation(
values=shap_values[:, :, 1], # SHAP-значения для второго класса
feature_names=X_test_enc.columns, # Имена признаков
data=X_test_enc.values # Исходные данные
)
# Визуализация важности признаков с подписями осей
fig, ax = plt.subplots(figsize=(10, 6)) # Создаём фигуру и ось
shap.plots.bar(shap_values_class_1, max_display=30, show=False)
# Добавляем подписи осей
ax.set_xlabel("Среднее абсолютное SHAP-значение", fontsize=12)
ax.set_ylabel("Входные признаки", fontsize=12)
ax.set_title("Важность признаков (SHAP)", fontsize=14, fontweight='bold')
plt.show() # Отображаем график
shap.plots.beeswarm(shap_values_class_1, max_display=30, show=False)
# Добавляем заголовок через `plt`
plt.title("Важность признаков (SHAP)", fontsize=14, fontweight='bold')
plt.xlabel("SHAP-значение (влияние на модель)", fontsize=12)
plt.ylabel("Входные признаки", fontsize=12)
plt.show() # Отображаем график
Для поиска лучшей модели был использован Pipeline содержащий следующие шаги:
- тренировочные и тестовые входные данные были закодированы с помощью:
MinMaxScaler,OneHotEncoder,OrdinalEncoder; - в процессе поиска к данным применено 4 типа моделей классификации:
DecisionTreeClassifier,SVC,LogisticRegressionиKNeighborsClassifier; - на основе метрики ROC-AUC была отобрана лучшая модель - SVC(C=10, gamma='auto', probability=True, random_state=42 и ядром
rbf), количественные данные которой были закодированыMinMaxScaler(), а категориальныеOneHotEncoder()иOrdinalEncoder(). Значение метрики ROC-AUC на тренировочной выборке равно ~ 0.9317;
Значение метрики ROC-AUC на тестовой выборке равно ~ 0.9272.
Так же были определены наиболее влияющие входные признаки на целевой quit - ими являются: job_satisfaction_rate, level, employment_years, и workload.
Модель SVC справилась лучше, чем LogisticRegression, т.к. один из входных признаков имеющих наибольшее влияние на целевой признак дает 'level', который связан с целевым нелинейно, это доказывает очень слабая линейная связь (коэф. корреляции = 0.31);
Если данные содержат сложные нелинейные связи, SVC может быть более подходящим, чем линейные модели, такие как LogisticRegression, или модели, основанные на расстояниях, как KNeighborsClassifier;
DecisionTreeClassifier может переобучаться, особенно если данные содержат шум или малое количество объектов. Это снижает его способность обобщать данные и уменьшает показатель ROC-AUC.
SVC же с правильно настроенной регуляризацией (C) способен игнорировать шум и искать обобщающую границу.
Общий вывод:¶
Для поиска лучшей модели, которая сможет предсказать уровень удовлетворённости сотрудника на основе данных заказчика предприняты шаги:
- провелена загрузка данных;
- проведена предобработка данных;
- проведено исследование полученных данных и признаков;
- была построена модель с метрикой `SMAPE` не более 15 на тестовой выборке;
- в процессе поиска к данным применено 3 типа моделей регрессии;
- на основе метрики `SMAPE` была отобрана лучшая модель `SVR` (C=1 и ядром `rbf`) c использованием пайплайна;
- метрика `SMAPE` лучшей модели на тренировочной выборке с прнименением кросс-валидации составила: **15.05**;
- метрика `SMAPE` лучшей модели на тренировочной выборке составила: **14.01**;
Так же были определены наиболее влияющие входные признаки на целевой job_satisfaction_rate - ими являются: salary, supervisor_evaluation, level и workload.
Для поиска лучшей модели, которая сможет предсказать на основе данных заказчика, что сотрудник уволится из компании предприняты шаги:
- провелена загрузка данных;
- проведена предобработка данных;
- проведено исследование полученных данных и признаков;
- была построена модель с метрикой `ROC-AUC` более 0.91 на тестовой выборке;
- в процессе поиска к данным применено 4 типа моделей классификации;
- на основе метрики `ROC-AUC` была отобрана лучшая модель `SVC` (C=10, gamma='auto', probability=True, random_state=42 и ядром `rbf`) c использованием пайплайна;
- метрика `ROC-AUC` лучшей модели на тренировочной выборке составила: **0.9327**;
- метрика `ROC-AUC` лучшей модели на тренировочной выборке составила: **0.9289**;
Так же были определены наиболее влияющие входные признаки на целевой quit - ими являются: job_satisfaction_rate, level, employment_years, и workload.
Был определен портрет увольняющегося сотрудника:
Сотрудник который увольняется работает недавно, у него зарплата ниже, чем у коллег на тех же должностях, его не повышают, он чаще нарушает правила, а начальство хуже его к нему относится. Так же он не удовлетворен работой (job_satisfaction_rate ниже, чем у других сотрудников).
Рекомендации для бизнеса по снижению уровня увольнения
Признаки удовлетворенность работой и вероятность увольнения очень сильно связаны, поэтому чтобы повысить удовлетворенность работой сотрудника необходимо:
- Следить за соответствием заработной платы сотрудника рынку;
- Чаще проводить конкурсы для повышения должностей сотрудников (должность напрямую влияет на пункт 1);
- Следить чтобы сотрудники были не сильно загружены - это может привести к выгоранию и последующим увольнением сотрудника.